'use strict'

var fs = require('fs'),
  union = require('union'),
  ecstatic = require('./ecstatic/ecstatic'),
  auth = require('basic-auth'),
  httpProxy = require('http-proxy'),
  corser = require('corser'),
  path = require('path'),
  secureCompare = require('secure-compare')

// a hacky and direct workaround to fix https://github.com/http-party/files-preview/issues/525
function getCaller() {
  try {
    var stack = new Error().stack
    var stackLines = stack.split('\n')
    var callerStack = stackLines[3]
    return callerStack.match(/at (.+) \(/)[1]
  } catch (error) {
    return ''
  }
}

var _pathNormalize = path.normalize
path.normalize = function (p) {
  var caller = getCaller()
  var result = _pathNormalize(p)
  // https://github.com/jfhbrook/node-ecstatic/blob/master/lib/ecstatic.js#L20
  if (caller === 'decodePathname') {
    result = result.replace(/\\/g, '/')
  }
  return result
}

//
// Remark: backwards compatibility for previous
// case convention of HTTP
//
exports.HttpServer = exports.HTTPServer = HttpServer

/**
 * Returns a new instance of HttpServer with the
 * specified `options`.
 */
exports.createServer = function (options) {
  return new HttpServer(options)
}

/**
 * Constructor function for the HttpServer object
 * which is responsible for serving static files along
 * with other HTTP-related features.
 */
function HttpServer(options) {
  options = options || {}

  if (options.root) {
    this.root = options.root
  } else {
    try {
      fs.lstatSync('./public')
      this.root = './public'
    } catch (err) {
      this.root = './'
    }
  }

  this.headers = options.headers || {}

  this.cache =
    options.cache === undefined
      ? 3600
      : // -1 is a special case to turn off caching.
      // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Preventing_caching
      options.cache === -1
      ? 'no-cache, no-store, must-revalidate'
      : options.cache // in seconds.
  this.showDir = options.showDir !== 'false'
  this.autoIndex = options.autoIndex !== 'false'
  this.showDotfiles = options.showDotfiles
  this.gzip = options.gzip === true
  this.brotli = options.brotli === true
  if (options.ext) {
    this.ext = options.ext === true ? 'html' : options.ext
  }
  this.contentType = options.contentType || this.ext === 'html' ? 'text/html' : 'application/octet-stream'

  var before = options.before ? options.before.slice() : []

  if (options.logFn) {
    before.push(function (req, res) {
      options.logFn(req, res)
      res.emit('next')
    })
  }

  if (options.username || options.password) {
    before.push(function (req, res) {
      var credentials = auth(req)

      // We perform these outside the if to avoid short-circuiting and giving
      // an attacker knowledge of whether the username is correct via a timing
      // attack.
      if (credentials) {
        // if credentials is defined, name and pass are guaranteed to be string
        // type
        var usernameEqual = secureCompare(options.username.toString(), credentials.name)
        var passwordEqual = secureCompare(options.password.toString(), credentials.pass)
        if (usernameEqual && passwordEqual) {
          return res.emit('next')
        }
      }

      res.statusCode = 401
      res.setHeader('WWW-Authenticate', 'Basic realm=""')
      res.end('Access denied')
    })
  }

  if (options.cors) {
    this.headers['Access-Control-Allow-Origin'] = '*'
    this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range'
    if (options.corsHeaders) {
      options.corsHeaders.split(/\s*,\s*/).forEach(function (h) {
        this.headers['Access-Control-Allow-Headers'] += ', ' + h
      }, this)
    }
    before.push(
      corser.create(
        options.corsHeaders
          ? {
              requestHeaders: this.headers['Access-Control-Allow-Headers'].split(/\s*,\s*/)
            }
          : null
      )
    )
  }

  if (options.robots) {
    before.push(function (req, res) {
      if (req.url === '/robots.txt') {
        res.setHeader('Content-Type', 'text/plain')
        var robots =
          options.robots === true ? 'User-agent: *\nDisallow: /' : options.robots.replace(/\\n/, '\n')

        return res.end(robots)
      }

      res.emit('next')
    })
  }


  before.push(
    ecstatic({
      root: this.root,
      cache: this.cache,
      showDir: this.showDir,
      showDotfiles: this.showDotfiles,
      autoIndex: this.autoIndex,
      defaultExt: this.ext,
      gzip: this.gzip,
      brotli: this.brotli,
      contentType: this.contentType,
      handleError: typeof options.proxy !== 'string'
    })
  )

  if (typeof options.proxy === 'string') {
    var proxy = httpProxy.createProxyServer({})
    before.push(function (req, res) {
      proxy.web(
        req,
        res,
        {
          target: options.proxy,
          changeOrigin: true
        },
        function (err, req, res, target) {
          if (options.logFn) {
            options.logFn(req, res, {
              message: err.message,
              status: res.statusCode
            })
          }
          res.emit('next')
        }
      )
    })
  }

  var serverOptions = {
    before: before,
    headers: this.headers,
    onError: function (err, req, res) {
      if (options.logFn) {
        options.logFn(req, res, err)
      }

      res.end()
    }
  }

  if (options.https) {
    serverOptions.https = options.https
  }

  this.server = union.createServer(serverOptions)
  if (options.timeout !== undefined) {
    this.server.setTimeout(options.timeout)
  }
}

HttpServer.prototype.listen = function () {
  this.server.listen.apply(this.server, arguments)
}

HttpServer.prototype.close = function () {
  return this.server.close()
}
